# _*_ coding: utf-8 _*_
import numpy as np
from numpy.linalg import norm
from copy import deepcopy
from time import time
from time import strftime
from time import gmtime
from math import exp
from math import floor
from random import uniform
from random import sample
from random import randint
from random import shuffle
from functools import wraps
import matplotlib.pyplot as plt


class Lifecycle:
    """
        Lifecycle of an optimizer running progress
    """

    def __init__(self, iterations=None, evaluations=None, deadline=None):
        self.iterations = int(1e9) if iterations is None else iterations
        self.evaluations = int(1e9) if evaluations is None else evaluations
        self.deadline = int(1e9) if deadline is None else deadline * 60  # minutes
        self.it = 0
        self.eva = 0
        self.__start_time = time()
        self.elapsed = time() - self.__start_time
        self.__pre = self.elapsed
        self.status = 'mounted'
        self.message = None
        self.gBest = []
        self.data = None

    def set_stopping_condition(self, iterations=None, evaluations=None, deadline=None):
        self.iterations = int(1e9) if iterations is None else iterations
        self.evaluations = int(1e9) if evaluations is None else evaluations
        self.deadline = int(1e9) if deadline is None else deadline * 60  # minutes

    def update(self, it=None, eva=None, timing=True, gBest=None, data=None):
        if it is not None:
            self.it += it
        if eva is not None:
            self.eva += eva
        if timing:
            self.elapsed = time() - self.__start_time
        if gBest is not None:
            self.gBest.append(gBest)
        self.data = data
        self.status = 'alive'

    def check_stopping_condition(self, mute=True):
        msg, stop = ['', '', ''], False
        if self.it >= self.iterations:
            msg[0] = 'iterations limitation reached. '
            stop = True
        if self.eva >= self.evaluations:
            msg[1] = 'evaluations limitation reached. '
            stop = True
        if self.elapsed >= self.deadline:
            msg[2] = 'deadline reached. '
            stop = True
        if stop:
            self.message = msg[0] + msg[1] + msg[2]
            self.status = 'closed'
            t = strftime('%H:%M:%S', gmtime(self.elapsed))
            if mute is False:
                print(f'Terminated at {self.it}, time cost {t}s')

        return stop

    def report(self, per_sec=True, graph=True, mapping=None):
        now = round(self.elapsed)
        if now != self.__pre or not per_sec:
            t = strftime('%H:%M:%S', gmtime(self.elapsed))
            print(f'{t} at {self.it:5d} route length {self.data[-1]:.2f}')
            self.__pre = now
            if graph:
                if mapping is not None:
                    plt.clf()
                    plt.subplot(1, 2, 1)
                    plt.plot(range(len(self.gBest)), self.gBest, linestyle='-', label='gBest')
                    plt.title(label=f'iterations {self.it}, d: {self.gBest[-1]:.2f}')
                    ax = plt.gca()
                    ax.spines['top'].set_color('none')
                    ax.spines['right'].set_color('none')
                    plt.legend()
                    plt.subplot(1, 2, 2)
                    route = mapping[0]
                    draw = mapping[1]
                    draw(plt, route, dot_size=20, show_text=True)
                    ax = plt.gca()
                    # ax.spines['top'].set_color('none')
                    # ax.spines['right'].set_color('none')
                    # ax.spines['left'].set_color('none')
                    # ax.spines['bottom'].set_color('none')
                    ax.set_aspect('equal')
                    plt.pause(0.001)
                else:
                    plt.clf()
                    plt.plot(range(len(self.gBest)), self.gBest, linestyle='-', label='gBest')
                    plt.title(label=f'iterations {self.it}, d: {self.gBest[-1]:.2f}')
                    plt.pause(0.001)


def timer(func):
    """
    a timer count the time for running functions
    :param func: target function
    :return:
    """

    @wraps(func)
    def wrapped(*args, **kwargs):
        start_time = time()
        result = func(*args, **kwargs)
        elapsed = time() - start_time
        print("func<{}> time cost -> {}s".format(func.__name__, elapsed))
        return result

    return wrapped


def minimal_insertion(distance_func, data_S, cities, routes):
    """
    @param distance_func:
    @param data_S: salesmen set for the specific city
    @param cities: cities to insert
    @param routes: the existing routes
    @return:
    """
    cities = cities[:]
    shuffle(cities)
    increased_cost = 0
    for city in cities:
        S = data_S[city]  # S is the accessible salesmen of the city.
        _s, _j, min_cost = -1, -1, 1e99
        for s in S:  # search in the route of each salesman
            route = routes[s - 1]
            for j in range(1, len(route)):
                cost = distance_func[route[j - 1]][city]\
                    + distance_func[city][route[j]] - distance_func[route[j - 1]][route[j]]
                if min_cost > cost:
                    min_cost = cost
                    _s, _j = s, j
        increased_cost += min_cost
        routes[_s - 1].insert(_j, city)
    return routes, increased_cost


def greedy_sequence(data, routes=None, candidates=None):
    distances = data.distances
    depot = data.depot
    if routes is None:
        routes = [[depot] for _ in range(0, data.m)]
    if candidates is None:
        candidates = data.cities[:]
        candidates.remove(depot)
    else:
        candidates = candidates[:]
    while True:
        if len(candidates) == 0:
            break
        for k, route in enumerate(routes):
            if len(candidates) == 0:
                break
            curr = route[-1]  # the last city in route k
            post, min_dis = -1, 1e99
            for city in candidates:
                if data.A[city][k + 1] == 1:
                    dis = distances[curr][city]
                    if min_dis > dis:
                        min_dis = dis
                        post = city
            if post != -1:
                route.append(post)
                candidates.remove(post)
    for route in routes:
        route.append(depot)

    return routes


def two_opt(route: list, distances):
    """
    @param route: route start and end with depot [d, ..., d]
    @param distances: distance matrix
    @return: optimized route
    """
    improve = 0
    for i in range(1, len(route) - 1):  # skip the first and the last (skip depot)
        _j, min_diff = -1, 1e99
        for j in range(i + 1, len(route) - 1):
            p, a = route[i - 1], route[i]
            b, n = route[j], route[j + 1]
            diff = distances[a][n] + distances[p][b] - distances[p][a] - distances[b][n]
            if min_diff > diff:
                min_diff = diff
                _j = j
        if min_diff < 0:
            route = route[:i] + route[_j:i - 1:-1] + route[_j + 1:]
            improve += min_diff
    if abs(improve) < 1e-10:
        improve = 0
    return route, improve

def jaccard(route1, route2):
    return len(set(route1) & set(route2)) / len(set(route1) | set(route2))

def edge_set(route: list):
    E = set()
    for i in range(1, len(route)):
        a, b = route[i-1], route[i]
        if a > b:
            a, b = b, a
        E.add((a, b))
    return E

def AB_cycle(A: list, B: list):
    pass

def edge_entropy(population):
    for p in population:
        pass
